// import Sortable, { type Options, type SortableEvent } from 'sortablejs'
import { type Options, type SortableEvent } from 'sortablejs'

// @ts-ignore
// import Sortable from '../deps/sortablejs'
//
import Sortable from '../Sortable/src/Sortable'
import type { Ref } from 'vue-demi'
import {
  getCurrentInstance,
  isRef,
  onMounted,
  onUnmounted,
  unref
} from 'vue-demi'
import type { Fn, RefOrElement, RefOrValue } from './types'

import { error } from './utils/log'

import {
  forEachObject,
  getElementBySelector,
  insertElement,
  insertNodeAt,
  isHTMLElement,
  isString,
  isUndefined,
  mergeOptionsEvents,
  needInsertDom,
  removeElement,
  removeNode,
  swapList,
  swapNodes
} from './utils'
import { nextTick, watch } from 'vue'

function defaultClone<T>(element: T): T {
  if (element === undefined || element === null) return element
  return JSON.parse(JSON.stringify(element))
}

function tryOnUnmounted(fn: Fn) {
  if (getCurrentInstance()) onUnmounted(fn)
}

function tryOnMounted(fn: Fn) {
  if (getCurrentInstance()) onMounted(fn)
  else fn()
}

const CLONE_ELEMENT_KEY = Symbol('cloneElement')

export interface DraggableEvent extends SortableEvent {
  item: HTMLElement & { [CLONE_ELEMENT_KEY]: any }
  swapDoms: { odd: HTMLElement[]; even: HTMLElement[] }
  maxDom: HTMLElement
  indexes: any
}

type SortableMethod = 'closest' | 'save' | 'toArray' | 'destroy' | 'option'

export interface UseDraggableReturn extends Pick<Sortable, SortableMethod> {
  /**
   * Start the sortable.
   * @param {HTMLElement} target - The target element to be sorted.
   * @default By default the root element of the VueDraggablePlus instance is used
   */
  start: (target?: HTMLElement) => void
  pause: () => void
  resume: () => void
}

export interface UseDraggableOptions<T> extends Options {
  clone?: (element: T) => T
  immediate?: boolean
  customUpdate?: (event: SortableEvent) => void
  onCustomSwap?: (event: SortableEvent) => void
}

/**
 * A custom hook that allows you to drag and drop elements in a list.
 * @param el
 * @param {Array} list - The list to be dragged
 * @param {Object} options - The options of the sortable
 * @returns {Object} - The return of the sortable
 */
export function useDraggable<T>(
  el: RefOrElement,
  list?: Ref<T[] | undefined>,
  options?: RefOrValue<UseDraggableOptions<T>>
): UseDraggableReturn
export function useDraggable<T>(
  el: null | undefined,
  list?: Ref<T[] | undefined>,
  options?: RefOrValue<UseDraggableOptions<T>>
): UseDraggableReturn
export function useDraggable<T>(
  el: Ref<HTMLElement | null | undefined>,
  options?: RefOrValue<UseDraggableOptions<T>>
): UseDraggableReturn
export function useDraggable<T>(
  el: null | undefined,
  options?: RefOrValue<UseDraggableOptions<T>>
): UseDraggableReturn

/**
 * 一个自定义钩子，允许你在列表中拖拽元素
 * @param {Ref<HTMLElement | null | undefined> | string} el - 拖拽的容器元素
 * @param {Ref<T[]>} list - 要被拖拽的列表
 * @param {RefOrValue<UseDraggableOptions<T>>} options - 拖拽功能的配置选项
 * @returns {UseSortableReturn} - 拖拽功能的返回值，类型为 UseSortableReturn
 */
export function useDraggable<T>(...args: any[]): UseDraggableReturn {
  const vm = getCurrentInstance()?.proxy

  const el = args[0]
  let [, list, options] = args

  if (!Array.isArray(unref(list))) {
    options = list
    list = null
  }

  let instance: Sortable | null = null
  const {
    immediate = true,
    clone = defaultClone,
    customUpdate
  } = unref(options) ?? {}

  /**
   * Element dragging started
   * @param {DraggableEvent} evt - DraggableEvent
   */
  function onStart(evt: DraggableEvent) {
    // console.log('onStart')
    evt.item[CLONE_ELEMENT_KEY] = clone(unref(unref(list)?.[evt.oldIndex!]))
  }

  /**
   * Element is dropped into the list from another list
   * @param {DraggableEvent} evt
   */
  function onAdd(evt: DraggableEvent) {
    // console.log('onAdd')
    const element = evt.item[CLONE_ELEMENT_KEY]
    if (isUndefined(element)) return
    removeNode(evt.item)
    insertElement(unref(list), evt.newDraggableIndex!, element)
  }

  /**
   * Element is removed from the list into another list
   * @param {DraggableEvent} evt
   */
  function onRemove(evt: DraggableEvent) {
    // console.log('onRemove')
    const { from, item, oldIndex, oldDraggableIndex, pullMode, clone } = evt
    if (pullMode === 'clone') {
      insertNodeAt(from, item, oldIndex!)
      removeNode(clone)
      return
    }
    removeElement(unref(list), oldDraggableIndex!)
  }

  /**
   * Changed sorting within list
   * @param {DraggableEvent} evt
   */
  let updateNum: number = 0
  let updateIndex: number | undefined = 0
  async function onUpdate(evt: DraggableEvent) {
    customSwapNum = 0
    customSwapIndex = ''

    if (updateIndex == evt.oldIndex) {
      updateNum++
    } else {
      updateIndex = evt.oldIndex
      updateNum = 0
    }

    if (updateNum < 2) return
    updateNum = 0
    updateIndex = 0

    if (customUpdate) {
      customUpdate(evt)
      return
    }
    const { from, item, oldIndex, oldDraggableIndex, newDraggableIndex } = evt
    const refElement = from.children[oldIndex!]
    swapNodes(refElement, item)

    if (isRef<any[]>(list)) {
      const newList = [...unref(list)]
      list.value = swapList(newList, oldDraggableIndex!, newDraggableIndex!)
      nextTick(() => {
        options.value.updatePosition()
      })
      return
    }
    swapList(unref(list), oldDraggableIndex!, newDraggableIndex!)
    nextTick(() => {
      options.value.updatePosition()
    })
  }

  interface DataModel {
    id: string
    logo?: string
    name?: string
    type: string
    url?: string
    widgetSize?: string
    fixed?: boolean
    class?: string
    widgetName?: string
    folder?: boolean
    children?: DataModel[]
    fill?: Array<number[]>

    [key: string]: unknown
  }

  // ---------------------------------
  let customSwapNum: number = 0
  let customSwapIndex: string | undefined = ''
  async function onCustomSwap(evt: DraggableEvent) {
    updateNum = 0
    updateIndex = 0

    const { swapDoms, maxDom, indexes } = evt

    const indexs = [...indexes.min.even, ...indexes.min.odd].toString()

    if (customSwapIndex == indexs) {
      customSwapNum++
    } else {
      customSwapIndex = indexs
      customSwapNum = 0
    }

    if (customSwapNum < 2) return
    customSwapNum = 0
    customSwapIndex = ''
    if (swapDoms && maxDom) {
      const parent = maxDom.parentNode!
      const maxDomNextSibling = maxDom.nextElementSibling

      const insertDom = needInsertDom(options, <Element>maxDom, {
        swapDoms,
        maxDom
      })

      let totalOddNum = 0
      indexes.min.odd.forEach((v: number) => {
        const maxData = options.value.utils.getSize(v)
        const [_rows, cols] = options.value.utils.getRowAndCol(
          maxData.widgetSize ? maxData.widgetSize : 'small'
        )
        totalOddNum += cols
      })
      const maxData = options.value.utils.getSize(indexes.max[0])
      const [, tmaxCol] = options.value.utils.getRowAndCol(
        maxData.widgetSize ? maxData.widgetSize : 'small'
      )
      // 不能交换, 多组件上下交换问题核心
      if (totalOddNum > tmaxCol) {
        // console.log(`useDraggable.ts内部, 229行`)
        return
      }

      // 处理DOM
      parent.insertBefore(maxDom, swapDoms.odd[0])

      swapDoms.odd.forEach(dom => {
        parent.insertBefore(dom, maxDomNextSibling)
      })

      if (insertDom) {
        swapDoms.even.forEach((dom: HTMLElement) => {
          parent.insertBefore(dom, insertDom)
        })
      } else {
        swapDoms.even.forEach(dom => {
          parent.appendChild(dom)
        })
      }

      // 处理数据插入
      const datas = {
        min: {
          odd: [],
          even: []
        },
        max: []
      } as {
        min: { odd: DataModel[]; even: DataModel[] }
        max: DataModel[]
      }

      for (let i = 0; i < indexes.min.odd.length; i++) {
        const d = options.value.utils.getSize(indexes.min.odd[i])
        datas.min.odd.push(d)
      }
      for (let i = 0; i < indexes.min.even.length; i++) {
        const d = options.value.utils.getSize(indexes.min.even[i])
        datas.min.even.push(d)
      }
      const d = options.value.utils.getSize(indexes.max[0])
      datas.max.push(d)
      // console.log('datas', datas, indexes)
      const newList = clone(unref(list))

      // 不推荐这样写, 但是这样效率最快
      newList.sort((itemA: any, itemB: any) => {
        const indexA = Array.from(maxDom.parentNode!.children).findIndex(
          (v: HTMLElement) => v.dataset!.id === itemA.id
        )
        const indexB = Array.from(maxDom.parentNode!.children).findIndex(
          (v: HTMLElement) => v.dataset!.id === itemB.id
        )
        return indexA - indexB
      })

      // console.log('newList', newList)

      list.value = newList
      options.value.updatePosition()

      // function findIndexContainsByPos(list: DataModel[], row: number, col: number) {
      //     return list.findIndex((v: DataModel) => v.fill && v.fill.length === 1 && v.fill.some(t => t[0] === row && t[1] === col))
      // }
      //
      // let fill = datas.max[0].fill!
      // let bPos = fill[fill.length / 2]
      // let i = bPos[0], j = bPos[1] - 1;
      // while (i >= 0) {
      //     while (j >= 0) {
      //         let insertDataIndex = findIndexContainsByPos(newList, i, j)
      //         if (insertDataIndex !== -1) {
      //             console.log('insertDataIndex 没换行', insertDataIndex)
      //             newList.splice(insertDataIndex + 1, 0, ...datas.min.even)
      //             await nextTick()
      //             console.log('clonedlist', newList)
      //             // list.value = newList;
      //             await options.value.updatePosition()
      //             return;
      //         }
      //         j--;
      //     }
      //     i--;
      //     j = options.value.utils.getRootVars.iconCol - 1;
      //     if (i < 0 || j < 0) return;
      //     console.log('clonedlist 换行', [i, j])
      //     let insertDataIndex = newList.findIndex((v: DataModel) => v.fill && v.fill.some(t => t[0] === i && t[1] === j))
      //     console.log('insertDataIndex', insertDataIndex)
      //     if (insertDataIndex !== -1) {
      //         newList.splice(insertDataIndex + 1, 0, ...datas.min.even)
      //         console.log('newList', newList)
      //         await nextTick()
      //         // list.value = newList;
      //         await options.value.updatePosition()
      //         return;
      //     }
      // }
    }
  }

  /**
   * preset options
   */
  const presetOptions: UseDraggableOptions<T> = {
    onUpdate,
    onStart,
    onAdd,
    onRemove,
    onCustomSwap
  }

  function getTarget(target?: HTMLElement) {
    const element = unref(el) as any
    if (!target) {
      target = isString(element)
        ? getElementBySelector(element, vm?.$el)
        : element
    }
    // @ts-ignore
    if (target && !isHTMLElement(target)) target = target.$el

    if (!target) error('Root element not found')
    return target
  }

  function mergeOptions() {
    // eslint-disable-next-line @typescript-eslint/no-unused-vars
    const { immediate, clone, ...restOptions } = unref(options) ?? {}
    return mergeOptionsEvents(
      list === null ? {} : presetOptions,
      restOptions
    ) as Options
  }

  const start = (target?: HTMLElement) => {
    target = getTarget(target)
    if (instance) methods.destroy()

    instance = new Sortable(target as HTMLElement, mergeOptions())
  }

  watch(
    () => options,
    () => {
      if (!instance) return
      forEachObject(mergeOptions(), (key, value) => {
        // @ts-ignore
        instance?.option(key, value)
      })
    },
    { deep: true }
  )

  const methods = {
    option: (name: keyof Options, value?: any) => {
      // @ts-ignore
      return instance?.option(name, value)
    },
    destroy: () => {
      instance?.destroy()
      instance = null
    },
    save: () => instance?.save(),
    toArray: () => instance?.toArray(),
    // @ts-ignore
    closest: (...args) => {
      // @ts-ignore
      return instance?.closest(...args)
    }
  } as Pick<Sortable, SortableMethod>

  const pause = () => methods?.option('disabled', true)
  const resume = () => methods?.option('disabled', false)

  tryOnMounted(() => {
    immediate && start()
  })

  tryOnUnmounted(methods.destroy)

  return { start, pause, resume, ...methods }
}
